/**
 * This file is Copyright © 2008 Software Craftsmen Limited. All Rights Reserved.
 */
package com.softwarecraftsmen.orogeny.filing;

import com.softwarecraftsmen.orogeny.filing.Folder.OrdinaryFolder;
import com.softwarecraftsmen.orogeny.filing.fileSystems.FileSystem;
import com.softwarecraftsmen.orogeny.filing.findFileFilters.FindFilesFilter;
import com.softwarecraftsmen.orogeny.filing.findSubdirectoriesFilters.FindSubdirectoriesFilter;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.File;
import java.io.FilenameFilter;
import java.nio.file.Files;
import java.util.*;

import static com.softwarecraftsmen.orogeny.filing.AbsoluteDirectories.absoluteDirectories;
import static com.softwarecraftsmen.orogeny.filing.AbsoluteDirectoriesAndFiles.absoluteDirectoriesAndFiles;
import static com.softwarecraftsmen.orogeny.filing.Folder.folder;
import static com.softwarecraftsmen.orogeny.filing.Folder.fromFile;
import static com.softwarecraftsmen.orogeny.filing.RelativeDirectory.relativeDirectory;
import static com.softwarecraftsmen.orogeny.filing.fileSystems.AbstractFileSystem.CurrentFileSystem;
import static com.softwarecraftsmen.orogeny.operatingSystems.OperatingSystemFamily.currentOperatingSystemFamily;
import static java.lang.String.format;
import static java.lang.System.getProperty;
import static java.nio.file.LinkOption.NOFOLLOW_LINKS;
import static java.util.Arrays.asList;
import static java.util.Arrays.sort;
import static java.util.Locale.UK;

public class AbsoluteDirectory extends AbstractAbsolutePath implements Directory
{
	private final FileSystem fileSystem;
	private final List<OrdinaryFolder> folders;

	@NotNull
	public static final AbsoluteDirectory CurrentWorkingDirectory = absoluteDirectoryFromSystemProperty("user.dir", CurrentFileSystem.root());

	@NotNull
	public static final AbsoluteDirectory CurrentUserHomeDirectory = absoluteDirectoryFromSystemProperty("user.home", CurrentWorkingDirectory);

	@NotNull
	public static final AbsoluteDirectory JavaHomeDirectory = absoluteDirectoryFromSystemProperty("java.home", CurrentWorkingDirectory);

	@NotNull
	public static final AbsoluteDirectory JavaIoTemporaryDirectory = absoluteDirectoryFromSystemProperty("java.io.tmpdir", CurrentWorkingDirectory);

	@Nullable
	public static final AbsoluteDirectory TMPDIR = safeAbsoluteDirectoryFromEnvironmentVariable("TMPDIR", null);

	@Nullable
	public static final AbsoluteDirectory HOME = safeAbsoluteDirectoryFromEnvironmentVariable("HOME", null);

	@Nullable
	public static final AbsoluteDirectory JAVA_HOME = safeAbsoluteDirectoryFromEnvironmentVariable("JAVA_HOME", null);

	@Nullable
	public static final AbsoluteDirectory OLDPWD = safeAbsoluteDirectoryFromEnvironmentVariable("OLDPWD", null);

	@Nullable
	public static final AbsoluteDirectory PWD = safeAbsoluteDirectoryFromEnvironmentVariable("PWD", null);

	@NotNull
	public static AbsoluteDirectory absoluteDirectoryFromSystemProperty(final @NotNull String systemProperty, final @NotNull AbsoluteDirectory defaultValue)
	{
		final String directory = getProperty(systemProperty, null);
		if (directory == null)
		{
			return defaultValue;
		}
		else
		{
			return CurrentFileSystem.toPath(CurrentFileSystem.root(), directory);
		}
	}

	@Nullable
	public static AbsoluteDirectory absoluteDirectoryFromEnvironmentVariable(final @NotNull String environmentVariableNameCaseSensitivityHandled, final @Nullable AbsoluteDirectory defaultValue)
	{
		final String path = currentOperatingSystemFamily().getEnvironmentVariableWithCaseSensitivityPerPlatform(environmentVariableNameCaseSensitivityHandled);
		if (path == null)
		{
			return defaultValue;
		}
		else
		{
			return CurrentFileSystem.toPath(CurrentFileSystem.root(), path);
		}
	}

	@Nullable
	private static AbsoluteDirectory safeAbsoluteDirectoryFromEnvironmentVariable(final @NotNull String environmentVariableNameCaseSensitivityHandled, final @Nullable AbsoluteDirectory defaultValue)
	{
		AbsoluteDirectory result;
		try
		{
			result = absoluteDirectoryFromEnvironmentVariable(environmentVariableNameCaseSensitivityHandled, defaultValue);
		}
		catch (Exception exception)
		{
			result = defaultValue;
		}
		return result;
	}

	private AbsoluteDirectory(final @NotNull FileSystem fileSystem, final @NotNull List<OrdinaryFolder> folders)
	{
		if (folders.isEmpty())
		{
			throw new IllegalArgumentException("folders is empty. Use an OrdinaryFolder of \"\" for the root");
		}
		this.fileSystem = fileSystem;
		this.folders = folders;
	}

	@NotNull
	public AbsolutePaths and(final @NotNull AbsolutePath absolutePath)
	{
		return absoluteDirectoriesAndFiles(this, absolutePath);
	}

	@NotNull
	public AbsoluteDirectories and(final @NotNull AbsoluteDirectory absoluteDirectory)
	{
		return absoluteDirectories(this, absoluteDirectory);
	}

	@NotNull
	public AbsoluteDirectoriesAndFiles and(final @NotNull AbsoluteFile absoluteFile)
	{
		return absoluteDirectoriesAndFiles(this, absoluteFile);
	}

	@NotNull
	public AbsolutePaths and(final @NotNull AbsolutePaths absolutePaths)
	{
		return absolutePaths.and(this);
	}

	@NotNull
	public AbsoluteDirectories and(final @NotNull AbsoluteDirectories absoluteDirectories)
	{
		return absoluteDirectories.and(this);
	}

	@NotNull
	public AbsoluteDirectoriesAndFiles and(final @NotNull AbsoluteFiles absoluteFiles)
	{
		return absoluteFiles.and(this);
	}

	@NotNull
	public AbsoluteDirectoriesAndFiles and(final @NotNull AbsoluteDirectoriesAndFiles absoluteDirectoriesAndFiles)
	{
		return absoluteDirectoriesAndFiles.and(this);
	}

	@NotNull
	public AbsoluteDirectory makeAbsolute(final @NotNull AbsoluteDirectory root)
	{
		return this;
	}

	@NotNull
	public AbsoluteDirectory subDirectory(final @NotNull String... folders)
	{
		return subDirectory(new ArrayList<Folder>()
		{{
			for (String folder : folders)
			{
				add(folder(folder));
			}
		}});
	}

	@NotNull
	public AbsoluteDirectory subDirectory(final @NotNull Folder... folders)
	{
		return subDirectory(asList(folders));
	}

	@NotNull
	public AbsoluteDirectory subDirectory(final @NotNull List<Folder> folders)
	{
		return subDirectory(new RelativeDirectory(folders));
	}

	@NotNull
	public AbsoluteDirectory subDirectory(final @NotNull RelativeDirectory relativeDirectory)
	{
		return relativeDirectory.makeAbsolute(this);
	}

	AbsoluteDirectory childSubDirectory(final @NotNull Folder folder)
	{
		if (folder.isCurrent())
		{
			return this;
		}
		if (folder.isParent())
		{
			if (folders.isEmpty())
			{
				throw new IllegalArgumentException("A parent folder can not be ascended into when there is not a parent to the path");
			}
			return new AbsoluteDirectory(fileSystem, folders.subList(0, folders.size() - 2));
		}
		return new AbsoluteDirectory(fileSystem, new ArrayList<OrdinaryFolder>(folders)
		{{
			add((OrdinaryFolder)folder);
		}});
	}

	@NotNull
	public OrdinaryFolder deepestFolder()
	{
		return folders.get(folders.size() - 1);
	}

	@NotNull
	public String toFileSystemSpecificPathPrefixedByPathSeparator()
	{
		return fileSystem.toFileSystemSpecificPathPrefixedByPathSeparator(folders);
	}

	@NotNull
	public String toFileSystemSpecificPathPrefixedByPathSeparator(final @NotNull String fileName)
	{
		return fileSystem.toFileSystemSpecificPathPrefixedByPathSeparator(folders, fileName);
	}

	@NotNull
	public String toFileSystemSpecificPathPostfixedByFolderSeparator()
	{
		return fileSystem.toFileSystemSpecificPathPostfixedByFolderSeparator(folders);
	}

	@NotNull
	public String toFileSystemSpecificPathPostfixedByPathSeparator()
	{
		return fileSystem.toFileSystemSpecificPathPostfixedByPathSeparator(folders);
	}

	@NotNull
	public String toFileSystemSpecificPathPostfixedByPathSeparator(final @NotNull String fileName)
	{
		return fileSystem.toFileSystemSpecificPathPostfixedByPathSeparator(folders, fileName);
	}

	@NotNull
	public String toFileSystemSpecificPath()
	{
		return fileSystem.toFileSystemSpecificPath(folders);
	}

	@NotNull
	public String toFileSystemSpecificPath(final @NotNull String fileName)
	{
		return fileSystem.toFileSystemSpecificPath(folders, fileName);
	}

	/**
	 *
	 * @param fileVisitor Visits files; files are provided sorted
	 */
	public void visitFiles(final @NotNull FileVisitor fileVisitor)
	{
		if (!exists())
		{
			return;
		}
		final String[] files = toFile().list(new FilenameFilter()
		{
			public boolean accept(final File file, final String name)
			{
				return !Files.isDirectory(new File(file, name).toPath(), NOFOLLOW_LINKS);
			}
		});
		sort(files);
		for (String file : files)
		{
			fileVisitor.visitFile(this, new FileName(file));
		}
	}

	public void visitFolders(final @NotNull FolderVisitor folderVisitor)
	{
		if (!exists())
		{
			return;
		}
		final String[] folders = toFile().list(new FilenameFilter()
		{
			public boolean accept(final File file, final String name)
			{
				final File file1 = new File(file, name);
				return Files.isDirectory(file1.toPath(), NOFOLLOW_LINKS);
			}
		});
		sort(folders);
		for (String folder : folders)
		{
			folderVisitor.visitFolder(subDirectory(folder(folder)));
		}
	}

	@NotNull
	public Set<AbsoluteFile> findFiles(final @NotNull FindFilesFilter findFilesFilter)
	{
		final Set<AbsoluteFile> foundFiles = new LinkedHashSet<>();
		internalFindFiles(findFilesFilter, foundFiles, this);
		return foundFiles;
	}

	@NotNull
	public Set<AbsoluteDirectory> findSubdirectories(final @NotNull FindSubdirectoriesFilter findSubdirectoriesFilter)
	{
		final Set<AbsoluteDirectory> foundSubdirectories = new LinkedHashSet<>();
		internalFindSubdirectories(findSubdirectoriesFilter, foundSubdirectories, this);
		return foundSubdirectories;
	}

	private void internalFindSubdirectories(final @NotNull FindSubdirectoriesFilter findSubdirectoriesFilter, final @NotNull Set<AbsoluteDirectory> foundSubdirectories, final @NotNull AbsoluteDirectory originalRoot)
	{
		visitFolders(new FolderVisitor()
		{
			public void visitFolder(final @NotNull AbsoluteDirectory subDirectory)
			{
				final RelativeDirectory relativeDirectory = originalRoot.relativeDirectoryAssumingAbsoluteDirectoryGivenIsSubdirectory(subDirectory);
				if (findSubdirectoriesFilter.include(originalRoot, relativeDirectory))
				{
					foundSubdirectories.add(subDirectory);
					subDirectory.internalFindSubdirectories(findSubdirectoriesFilter, foundSubdirectories, originalRoot);
				}
			}
		});
	}

	private void internalFindFiles(final @NotNull FindFilesFilter findFilesFilter, final @NotNull Set<AbsoluteFile> foundFiles, final @NotNull AbsoluteDirectory originalRoot)
	{
		visitFolders(new FolderVisitor()
		{
			public void visitFolder(final @NotNull AbsoluteDirectory subDirectory)
			{
				subDirectory.internalFindFiles(findFilesFilter, foundFiles, originalRoot);
			}
		});

		final RelativeDirectory relativeDirectory = originalRoot.relativeDirectoryAssumingAbsoluteDirectoryGivenIsSubdirectory(this);
		visitFiles(new FileVisitor()
		{
			public void visitFile(final @NotNull AbsoluteDirectory subDirectory, final @NotNull FileName fileName)
			{
				final boolean include = findFilesFilter.include(originalRoot, relativeDirectory, fileName);
				if (include)
				{
					foundFiles.add(new AbsoluteFile(subDirectory, fileName));
				}
			}
		});
	}

	@NotNull
	public RelativeDirectory relativeDirectoryAssumingAbsoluteFileGivenIsSubdirectory(final @NotNull AbsoluteFile absoluteFile)
	{
		return absoluteFile.relativeDirectoryToRootGiven(this);
	}

	@NotNull
	public RelativeDirectory relativeDirectoryAssumingAbsoluteDirectoryGivenIsSubdirectory(final @NotNull AbsoluteDirectory subDirectory)
	{
		if (subDirectory.folders.size() < this.folders.size())
		{
			throw new IllegalArgumentException(format(UK, "subDirectory %1$s is shallower than parent %2$s", subDirectory, this));
		}
		for(int index = 0; index < this.folders.size(); index++)
		{
			final OrdinaryFolder thisFolder = this.folders.get(index);
			final OrdinaryFolder subDirectoryFolder = subDirectory.folders.get(index);
			if (!thisFolder.equals(subDirectoryFolder))
			{
				throw new IllegalArgumentException(format(UK, "subDirectory %1$s does not match parent %2$s", subDirectory, this));
			}
		}
		final List<Folder> relativeFolders = new ArrayList<>();
		for(int subFolderIndex = this.folders.size(); subFolderIndex < subDirectory.folders.size(); subFolderIndex++)
		{
			relativeFolders.add(subDirectory.folders.get(subFolderIndex));
		}
		return new RelativeDirectory(relativeFolders);
	}

	public boolean exists()
	{
		return toFile().exists() && isDirectory();
	}

	private boolean isDirectory()
	{
		return toFile().isDirectory();
	}

	public boolean existsAndIsHidden()
	{
		return exists() && fileSystem.isHidden(folders);
	}

	public boolean exists(final @NotNull FileName fileName)
	{
		return fileName.exists(this);
	}

	public boolean existsAndIsHidden(final @NotNull FileName fileName)
	{
		return fileName.exists(this) && fileName.isHidden(fileSystem, folders);
	}

	public void makeAllFoldersNecessary() throws IsNotADirectoryException, CouldNotMakeAllFoldersNecessaryException
	{
		if (exists())
		{
			return;
		}
		final File file = toFile();
		if (file.exists() && !isDirectory())
		{
			throw new IsNotADirectoryException(this);
		}
		final boolean succeeded = file.mkdirs();
		if (!succeeded)
		{
			throw new CouldNotMakeAllFoldersNecessaryException(this);
		}
	}

	public void delete() throws IsNotADirectoryException, CouldNotDeleteFileException, CouldNotDeleteFolderException
	{
		if (!isDirectory())
		{
			throw new IsNotADirectoryException(this);
		}
		if (!exists())
		{
			return;
		}

		deleteFolders();
		deleteFiles();

		final boolean deleted = toFile().delete();
		if (!deleted)
		{
			throw new CouldNotDeleteFolderException(this);
		}
	}

	private void deleteFolders() throws CouldNotDeleteFolderException
	{
		final CouldNotDeleteFolderException[] exception = new CouldNotDeleteFolderException[1];
		visitFolders(new FolderVisitor()
		{
			public void visitFolder(final @NotNull AbsoluteDirectory folderDirectory)
			{
				try
				{
					folderDirectory.delete();
					exception[0] = null;
				}
				catch (FilingOperationFailedException e)
				{
					exception[0] = new CouldNotDeleteFolderException(AbsoluteDirectory.this, e);
				}
			}
		});
		if (exception[0] != null)
		{
			throw exception[0];
		}
	}

	private void deleteFiles() throws CouldNotDeleteFolderException
	{
		final CouldNotDeleteFolderException[] exception = new CouldNotDeleteFolderException[1];
		visitFiles(new FileVisitor()
		{
			public void visitFile(final @NotNull AbsoluteDirectory root, final @NotNull FileName fileName)
			{
				try
				{
					fileName.delete(root);
					exception[0] = null;
				}
				catch (FilingOperationFailedException e)
				{
					exception[0] = new CouldNotDeleteFolderException(AbsoluteDirectory.this, e);
				}
			}
		});
		if (exception[0] != null)
		{
			throw exception[0];
		}
	}

	@NotNull
	public File toFile()
	{
		return new File(toFileSystemSpecificPath());
	}

	@NotNull
	public AbsoluteDirectories parsePathsDescription(final @NotNull String pathsDescription)
	{
		return fileSystem.toPaths(this, pathsDescription);
	}

	@SuppressWarnings({"ToArrayCallWithZeroLengthArrayArgument"})
	@NotNull
	public static AbsoluteDirectory absoluteDirectory(final @NotNull FileSystem fileSystem, final @NotNull File directoryPath)
	{
		if (directoryPath.exists() && directoryPath.isFile())
		{
			throw new IllegalArgumentException(format(UK, "%1$s is a file", directoryPath));
		}
		if (!directoryPath.isAbsolute())
		{
			return relativeDirectory(fileSystem, directoryPath).makeAbsolute(CurrentWorkingDirectory);
		}
		return absoluteDirectory(fileSystem, fromFile(fileSystem, directoryPath).toArray(new Folder[]{}));
	}

	@NotNull
	public static AbsoluteDirectory absoluteDirectory(final @NotNull FileSystem fileSystem, final @NotNull Folder... folders)
	{
		return absoluteDirectory(fileSystem, asList(folders));
	}

	@NotNull
	public static AbsoluteDirectory absoluteDirectory(final @NotNull FileSystem fileSystem, final @NotNull List<Folder> folders)
	{
		final Stack<OrdinaryFolder> neededFolders = new Stack<>();
		if (folders.size() == 0)
		{
			neededFolders.push(fileSystem.rootFolder());
		}
		else if (!fileSystem.rootFolder().equals(folders.get(0)))
		{
			neededFolders.push(fileSystem.rootFolder());
		}
		for (Folder folder : folders)
		{
			if (folder.isCurrent())
			{
				continue;
			}
			if (folder.isParent())
			{
				if (neededFolders.isEmpty())
				{
					throw new IllegalArgumentException("A parent folder can not be ascended into when there is not a parent to the path");
				}
				neededFolders.pop();
			}
			if (folder.isOrdinary())
			{
				neededFolders.push((OrdinaryFolder) folder);
			}
		}
		return new AbsoluteDirectory(fileSystem, new ArrayList<>(neededFolders));
	}

	@NotNull
	public String toString()
	{
		return toFileSystemSpecificPath();
	}

	public boolean equals(final @Nullable Object o)
	{
		if (this == o)
		{
			return true;
		}
		if (o == null || getClass() != o.getClass())
		{
			return false;
		}

		final AbsoluteDirectory that = (AbsoluteDirectory) o;
		return fileSystem.equals(that.fileSystem) && folders.equals(that.folders);
	}

	public int hashCode()
	{
		int result;
		result = fileSystem.hashCode();
		result = 31 * result + folders.hashCode();
		return result;
	}

	@NotNull
	public AbsoluteFile file(final @NotNull String fileName)
	{
		return file(new FileName(fileName));
	}

	@NotNull
	public AbsoluteFile file(final @NotNull FileName fileName)
	{
		return new AbsoluteFile(this, fileName);
	}

	public void setDirectory(final @NotNull ProcessBuilder processBuilder)
	{
		processBuilder.directory(toFile());
	}

	public boolean isRoot()
	{
		return folders.size() == 1;
	}

	@NotNull
	public AbsoluteDirectory parent()
	{
		if (isRoot())
		{
			return this;
		}
		else
		{
			return new AbsoluteDirectory(fileSystem, folders.subList(0, folders.size() - 1));
		}
	}

}
